<?php

/** TClient
@author Dmitry Zegebart keksov@gmail.com
@date 04.2008
@abstract Typical Javascript application may consists of two parts: (semi)static library files which are not changed by server side scripts
on every request and dynamic files which may be changed by server on every request. If you have somewhere in your files code something like
<script> var a = < ?php echo time() ? > </script> (note thh PHP insertion of current time value) and this file most likely has php extension,
then this script should be treated as dynamic. TClient helps you to organize your script files, compress and cache it for reducing
load on server and speeding up loading of your pages. How to use: adjust default values of constant settings according to your needs 
and tailor lists of files in getDynamicScript(), getStaticScript() and getCSS(). The only public function you should call is TClient.send() it
will generate two types of output: HTML tag <script> and <link rel="stylesheet"...> for usage in your index.php page or script body itself, 
depending on the way you invoke this file.

Your minimal index.php may be like this:
<html> 
<head>
    <?php 
        include_once( "THTMLPage.php" ); // you have to uncomment last line in this file
    ?>
</head>
<body/>
</html>

*/

/***************************************************************************
* THTMLPage
***************************************************************************/
class THTMLPage
{ /* Consider the following while configuring parameters :
    1. Most of modern browsers support gzip compression of all data which is send by server to the user.
    2. You may save considerable amount of your valuable traffic by compressing your static scripts, css and html.
    3. On the other hand, calling compression routines for every dynamically created data on every request may lead
       to server overload.
    4. So, divide your web application into to two parts static (css, javascript) and dynamic (html, some javascript)
       browser will cache your static content on user's PC, so when he/she/it returns to your site the content will be not fetched
       from your server but from user's cache file.
    5. Caching of compressed static context on server helps reducing server load while processing requests from new users and
       reduces overall site loading time.

    TClient class has several options, you have to adjust it depending on environment (production or develop) you use it in.
    Then most notable moments you have to pay your attention to:
        * fill getStaticScript(), getDynamicScript() and getCSS() with lists of your files.
        * set $hasStaticScript, $hasDynamicScript and $hasCSS flags
        * verify $mode value
        * manage $scriptVersion and $cssVersion if in PRODUCTION_MODE
    
    Read in-code comments for more information.
*/

    // set mode to DEVELOP_MODE if you actively editing and testing your script. In this mode all cache and compress functions are ignored
    // Change it to PRODUCTION_MODE for deployment on production server, caching and on the fly compression will be activated.
    public         $mode                      = self::PRODUCTION_MODE;
    // Version of static part of your script. If mode is 'PRODUCTION_MODE' then you have to increase it every time you publish new version.
    // Otherwise your browser may not update it's cache and your users'll not see your changes on return. 
    // If mode is 'DEVELOP_MODE' you may not touch it until you got a release version for production server. There is one exception to this rule-
    // if you have an active breakpoint in firebug window, editing your script at the same time and pressing Ctrl-R the page may not be reloaded,
    // so you have to update this value. The same could be true if you test your scripts via some HTTP proxy, in such a case I recommend 
    // set $forceNoBrowserCache to true, but it has drawbacks too- if you have some breakpoints in firebug and load a new copy of you script, then
    // these breakpoint are no longer present in firebug window. The magic is done in sendHTMLTag, note the GET parameters and it's values after '?'
    public         $scriptVersion             = '0.0';
    // true - if your application has js files which are not altered by server side scripts on every request. I recommend splitting your application
    // into two parts: static and dynamic. Javascript code in dynamic part may be altered by server side scripts on every request, so
    // we can't cache it. Adjust lists in getDynamicScript and getStaticScript accordingly
    public         $hasStaticScript           = false;
    
    // you may apply compression to CSS files too
    public         $hasCSS                    = false;
    // version of css files (read comments on $      Version parameter for more information) 
    public         $cssVersion                = '0.0';
    
    // in typical Javascript application we have no dynamic part
    public         $hasDynamicScript          = false;
    // you may compress dynamicPart also, but it may put additional (heavy) load on your server.
    public         $dynamicCompressionEnable  = false;
    
    // you may compress body part too, but it may put additional (heavy) load on your server.
    public         $hasBody                   = false;
    // you may cache main content of your site too, in some cases it may reduce CPU usage of your site. For exaple you have an AJAX application
    // and your main html is a static page. Don't forget to change bodyVersion in this case after html update.
    public         $bodyServerCacheEnable     = false;
    // version of your body part. Use it if bodyServerCacheEnable set to true
    public         $bodyVersion               = '0.0';
    
    // directory for storing cached files, it should exists and be writeable by this script (server process)
    public         $gzCacheBase               = 'var';
    // When you are in 'DEVELOP_MODE' caching of the script is done by the browser according to the header( "Expires: Mon, 26 Jul 2010 05:00:00 GMT" );
    // if you for some reason need a fresh output on every request with no caching (by browser) at all set $forceNoBrowserCache to true 
    // (see outputHTMLTag for a trick)
    public         $forceNoBrowserCache       = false;
    // some additional information, used in name of cached files. For example you may have localisation on your site
    // and for every locale setting you need a separate cache file
    public         $tag                       = '';
    // charset of your files
    public         $charset                   = 'ISO-8859-1';
    // name of PHP script which name is used in getXXXTag functions.
    public         $serverScriptName          = 'page.php';
    
    // Nothing to change below this line
    private        $outputType                = self::HEAD;
    private        $contentEncoding           = 'application/x-javascript';
    private        $gzipAccepted              = false;
    private        $contentType               = 'text/javascript';
    
    const          DYNAMIC_SCRIPT             = 'd';
    const          STATIC_SCRIPT              = 's';
    const          CSS                        = 'c';
    const          BODY                       = 'b';
    
    const          HEAD                       = 'h';
    const          DEVELOP_MODE               = 'd';
    const          PRODUCTION_MODE            = 'p';
    
    /***************************************************************************
    * compressClientOutput
    ***************************************************************************/
    protected function init()
    {
        if ( ( $this->outputType == self::DYNAMIC_SCRIPT || isset( $_REQUEST[ self::DYNAMIC_SCRIPT ] ) ) && $this->hasDynamicScript )
        {   
            $this->outputType      = self::DYNAMIC_SCRIPT;
            $this->contentType     = 'text/javascript';
            $this->contentEncoding = 'application/x-javascript';
        }
        else if ( ( $this->outputType == self::STATIC_SCRIPT || isset( $_REQUEST[ self::STATIC_SCRIPT ] ) ) && $this->hasStaticScript )
        {
            $this->outputType      = self::STATIC_SCRIPT;
            $this->contentType     = 'text/javascript';
            $this->contentEncoding = 'application/x-javascript';
        }
        else if ( ( $this->outputType == self::CSS || isset( $_REQUEST[ self::CSS ] ) ) && $this->hasCSS )
        {
            $this->outputType      = self::CSS;
            $this->contentType     = 'text/css';
            $this->contentEncoding = 'text/css';
        }
        else if ( $this->hasBody && $this->outputType = self::BODY )
        {
            $this->outputType      = self::BODY;
            $this->contentType     = 'text/html';
            $this->contentEncoding = 'text/html';
        }
        else 
        {
            $this->outputType = self::HEAD;
            return;
        }
        
        // see if user can handle gzipped data
        $accept = str_replace( " ", "", strtolower( $_SERVER[ 'HTTP_ACCEPT_ENCODING' ] ) );
        $accept = explode( ",", $accept );
        
        $this->gzipAccepted = in_array( "gzip", $accept )
                              && $this->mode != self::DEVELOP_MODE 
                              && ( $this->dynamicCompressionEnable && self::$outputType == self::DYNAMIC_SCRIPT && $this->mode == self::PRODUCTION_MODE
                                   || $this->outputType != self::DYNAMIC_SCRIPT );
        
        if ( $this->gzipAccepted )
        {
            $this->contentEncoding = 'gzip';
        }
    }

    /***************************************************************************
    * send
    ***************************************************************************/
    public function send( $aType = '' )
    {
        $this->outputType = $aType;
        $this->init();
        
        if ( $this->outputType == self::HEAD )
        { // script is included from server side via 'include' or similar directive, we have to output html '<script>' tag
            $this->sendHeadTag();
            return;
        }
        
        // script is called directly from browser via HTTP request, we have to provide script's body itself
        $this->sendContent();
    }
    
    /***************************************************************************
    * getStaticTag
    ***************************************************************************/
    protected function getStaticScriptTag()
    {
        if ( !$this->hasStaticScript )
        { 
            return;
        }
        
        // compressed and cached part of our application. Don't forget to increment version of TClient::$scriptVersion 
        // if you changed something in your static scripts while in PRODUCTION_MODE
        $s = $this->scriptVersion;
        if ( $this->forceNoBrowserCache )
        { // adding this parameter to 'src' part of 'script' tag will
          // force browser to (re)fetch a new copy of our script from the server
            $s .= time() % 10000;
        }
        $s .= $this->getStaticHTMLParam();
        
        echo '<script type="text/javascript" language="javascript" src="'.$this->serverScriptName.'?'.self::STATIC_SCRIPT.'='.$s.'"></script>';
    }
    
    /***************************************************************************
    * getStaticHTMLParam
    ***************************************************************************/
    protected function getStaticHTMLParam()
    { // OVERRIDE ME
        return '';
    }
    
    /***************************************************************************
    * getCSSTag
    ***************************************************************************/
    protected function getCSSTag()
    {
        if ( !$this->hasCSS )
        { 
            return;
        }
        
        $s = $this->cssVersion;
        if ( $this->forceNoBrowserCache )
        { 
            $s .= time() % 10000;
        }
        $s .= $this->getCSSHTMLParam();
        
        echo '<link rel="stylesheet" type="text/css" href="'.$this->serverScriptName.'?'.self::CSS.'='.$s.'" />';
    }
    
    /***************************************************************************
    * getCSSHTMLParam
    ***************************************************************************/
    protected function getCSSHTMLParam()
    { // OVERRIDE ME
        return '';
    }
    
    /***************************************************************************
    * getDynamicTag
    ***************************************************************************/
    protected function getDynamicScriptTag()
    {
        if ( !$this->hasDynamicScript )
        { 
            return;
        }
        
        $param = $this->getDynamicHTMLParam();
        // never cache this part of the application
        $noCache = time() % 10000;
        // by adding (semi)random $noCache parameter to 'src' we will force browser to (re)load the fresh copy of our dynamic data on every page's reload
        echo '<script type="text/javascript" language="javascript" src="'.$this->serverScriptName.'?'.self::DYNAMIC_SCRIPT.'='.$noCache.$param.'"></script>';
    }
    
    /***************************************************************************
    * getDynamicHTMLParam
    ***************************************************************************/
    protected function getDynamicHTMLParam()
    { // OVERRIDE ME
        return '';
    }
    
    /***************************************************************************
    * sendHeadTag
    ***************************************************************************/
    public function sendHeadTag()
    {
        $this->getCSSTag();
        $this->getStaticScriptTag();
        $this->getDynamicScriptTag();
    }
    
    /***************************************************************************
    * getContentEnconding
    ***************************************************************************/
    private function getContentEnconding()
    {
        return ( "Content-Encoding: ".$this->contentEncoding );
    }
    
    /***************************************************************************
    * getContentType
    ***************************************************************************/
    private function getContentType()
    {
        return ( "Content-Type: ".$this->contentType.'; charset='.$this->charset );
    }
    
    /***************************************************************************
    * sendContent
    ***************************************************************************/
    private function sendContent()
    {
        // instruct the browser to cache our data forever. That's why it is so important to change value of 
        // $scriptVersion and $cssVersion for every new release in PRODUCTION_MODE- it will force user's browser to update cache
        if ( $this->outputType != self::BODY ) 
        {
            header( 'Expires: Mon, 26 Jul 2100 05:00:00 GMT' );
        }
        
        header( 'Vary: Accept-Encoding' );
        header( $this->getContentEnconding() );
        header( $this->getContentType() ); //..;charset=UTF-8" );

        $this->getContent();
    }
    
    /***************************************************************************
    * getContent
    ***************************************************************************/
    private function getContent()
    {
        // dynamic part
        
        if ( $this->outputType == self::DYNAMIC_SCRIPT )
        { // dynamic part is always fresh, no server side cache.
            if ( $this->mode == self::DEVELOP_MODE )
            {
                $this->getDynamicScript();
                return;
            }

            if ( $this->gzipAccepted )
            {
                ob_start();
                $this->getDynamicScript();
                $data = ob_get_contents();
                ob_end_clean();
                
                $this->compressDynamicOutput( $data );
                return;
            }
            
            $this->getDynamicScript();
            return;
        }
        
        // static part
        
        if ( $this->outputType == self::STATIC_SCRIPT )
        {
            if ( $this->mode == self::DEVELOP_MODE )
            {
                $this->getStaticScript();
                return;
            }
            
            // static part may be cached in file(s) on server
            $outputFileName = $this->gzCacheBase."/".$this->tag.$this->scriptVersion.".js";
            if ( $this->gzipAccepted )
            {
                $outputFileName .= '.gz';
            }
            
            if ( file_exists( $outputFileName ) )
            {
                readfile( $outputFileName );
                return;
            }
            
            ob_start();
            $this->getStaticScript();
            $data = ob_get_contents();
            ob_end_clean();
            
            $this->compressStaticOutput( $data );
            return;
        }
        
        // css part
        
        if ( $this->outputType == self::CSS )
        {
            if ( $this->mode == self::DEVELOP_MODE )
            {
                $this->getCSS();
                return;
            }
            
            // static part may be cached in file(s) on server
            $outputFileName = $this->gzCacheBase."/".$this->tag.$this->cssVersion.".css";
            if ( $this->gzipAccepted )
            {
                $outputFileName .= '.gz';
            }
            
            if ( file_exists( $outputFileName ) )
            {
                readfile( $outputFileName );
                return;
            }
            
            ob_start();
            $this->getCSS();
            $data = ob_get_contents();
            ob_end_clean();
            
            $this->compressCSSOutput( $data );
            return;
        }
        
        // body part
        
        if ( $this->outputType == self::BODY )
        {
            /*ob_start('ob_gzhandler');
            $this->getBody();
            //$data = ob_get_contents();
            ob_end_flush();
            return;*/
            
            if ( $this->mode == self::DEVELOP_MODE || !$this->gzipAccepted && !$this->bodyServerCacheEnable )
            {
                ob_start();
                $this->getBody();
                ob_end_flush();
                return;
            }
            
            // static part may be cached in file(s) on server
            if ( $this->bodyServerCacheEnable )
            {
                $outputFileName = $this->gzCacheBase."/".$this->tag.$this->bodyVersion.".html";
                if ( $this->gzipAccepted )
                {
                    $outputFileName .= '.gz';
                }
                
                if ( file_exists( $outputFileName ) )
                {
                    readfile( $outputFileName );
                    return;
                }
            }
            
            /*ob_start('ob_gzhandler');
            $this->getBody();
            //$data = ob_get_contents();
            ob_end_flush();
            
            //$this->compressBodyOutput( $data );*/
            
            ob_start();
            $this->getBody();
            $data = ob_get_contents();
            ob_end_clean();
            
            $this->compressBodyOutput( $data );
            return;
        }
    }

    /***************************************************************************
    * compressStaticOutput
    ***************************************************************************/
    private function compressData( &$aData, &$aFileName )
    {
        // write a cache file for users with no compression support
        $fp = fopen( $aFileName, "w" );
        fwrite( $fp, $aData );
        fclose( $fp );

        // write a gzipped version of our script to corresponding cache file
        $aFileName .= '.gz';
        
        // compress data and write it to cache file on server
        $gzdata = $this->gzip( $aData, 9 );
        $fp = fopen( $aFileName, "w" );
        fwrite( $fp, $gzdata );
        fclose( $fp );
        
        // send data to user
        if ( $this->gzipAccepted )
        {
            echo $gzdata;
        }
        else
        {
            echo $aData;
        }
    }
    
    /***************************************************************************
    * compressStaticOutput
    ***************************************************************************/
    public function compressStaticOutput( $aScriptBody )
    {
        $outputFileName = $this->gzCacheBase."/".$this->tag.$this->scriptVersion.".js";
        $this->compressData( $aScriptBody, $outputFileName );
    }
    
    /***************************************************************************
    * compressCSSOutput
    ***************************************************************************/
    public function compressCSSOutput( $aCSSBody )
    {
        $outputFileName = $this->gzCacheBase."/".$this->tag.$this->cssVersion.".css";
        $this->compressData( $aCSSBody, $outputFileName );
    }
    
    /***************************************************************************
    * compressDynamicOutput
    ***************************************************************************/
    public function compressDynamicOutput( $aScriptBody )
    {
        //$this->gzip( $aScriptBody, 9 );
        $gzdata = $this->gzip( $aScriptBody, 9 );
        echo $gzdata;
    }
    
    /***************************************************************************
    * compressBodyOutput
    ***************************************************************************/
    public function compressBodyOutput( $aBody )
    {
        if ( $this->bodyServerCacheEnable )
        {
            $outputFileName = $this->gzCacheBase."/".$this->tag.$this->bodyVersion.".html";
            $this->compressData( $aBody, $outputFileName );
        }
        else
        {
            $gzdata = $this->gzip( $aBody, 9 );
            echo $gzdata;
        }
    }
    
    /***************************************************************************
    * gzip
    ***************************************************************************/
    private function gzip( $data = "", $level = 9, $filename = "", $comments = "" )
    {
        $flags = ( empty($comment)? 0 : 16) + ( empty($filename)? 0 : 8 );
        $mtime = time();
       
        return( pack("C1C1C1C1VC1C1", 0x1f, 0x8b, 8, $flags, $mtime, 2, 0xFF) .
                     ( empty($filename) ? "" : $filename . "\0" ) .
                     ( empty($comment) ? "" : $comment . "\0" ) .
                     gzdeflate( $data, $level ) .
                     pack( "VV", crc32($data), strlen($data) ) );
    }

    /***************************************************************************
    * getDynamicScript
    ***************************************************************************/
    protected function getDynamicScript()
    { // these files may be changed by server side scripts, so we should use 'include' to let the server parse it
        //include_once( "client/dynamic.js" );
        
        // Edit the list above or override this method in your custom class which extends TClient
    }
    
    /***************************************************************************
    * getDynamicScript
    ***************************************************************************/
    protected function getStaticScript()
    { // content of these files is static, so we can use readfile to bypass any parsing by server and send them directly to user
        //readfile( "client/static.js" );
        
        // Edit the list above or override this method in your custom class which extends TClient
    }
    
    /***************************************************************************
    * getCSS
    ***************************************************************************/
    protected function getCSS()
    { // content of these files is static, so we can use readfile to bypass any parsing by server and send them directly to user
        // readfile( "css/my.css" );
        
        // Edit the list above or override this method in your custom class which extends TClient
    }
    
    /***************************************************************************
    * getBody
    ***************************************************************************/
    protected function getBody()
    { // OVERRIDE ME
    }
}

// Uncomment next lines if you use this file directly without extending it with your own class
//$page = new THTMLPage();
// By default it will call sendHTMLTag()
//$page->send();

?>